/*
 * Copyright (C) 2014 Steven Kurilovitch
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.goodjaerb.swccg.ui;

import com.goodjaerb.swccg.core.filter.FilterField;
import com.goodjaerb.swccg.core.CardData;
import com.goodjaerb.swccg.core.DeckListItem;
import com.goodjaerb.swccg.core.dnd.TransferableDeckListItem;
import com.goodjaerb.swccg.core.filter.SideFilter;
import com.goodjaerb.swccg.core.filter.Filter;
import com.goodjaerb.swccg.core.filter.ListFilter;
import com.goodjaerb.swccg.core.filter.TextFilter;
import com.goodjaerb.swccg.core.filter.ValidFieldFilter;
import com.goodjaerb.swccg.ui.CardPanel.IconPanel;
import java.awt.AWTEvent;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragSource;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.RoundRectangle2D;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.DefaultListModel;
import javax.swing.DropMode;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JLayer;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.TransferHandler;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.filechooser.FileFilter;
import javax.swing.plaf.LayerUI;
import org.jdesktop.core.animation.timing.Animator;
import org.jdesktop.core.animation.timing.TimingTarget;
import org.jdesktop.core.animation.timing.TimingTargetAdapter;
import org.jdesktop.swing.animation.timing.sources.SwingTimerTimingSource;

/**
 *
 * @author Steven Kurilovitch
 */
public class DeckBuilderFrame extends javax.swing.JFrame {
    
    private static final String HOLOTABLE_PATH_PREF = "SWCCG_DECKBUILDER_HOLOTABLE_PATH";
    private static final String PREVIOUS_OPEN_PATH_PREF = "SWCCG_DECKBUILDER_PREVIOUS_OPEN_PATH";
    private String holotablePath;
    private static CardSheet cardSheet1;
    private List<Filter> filters;
    private SheetLayerUI layerUI;
    private JLayer<JComponent> cardSheetLayer;
    
    private final EnumMap<FilterField, Set<String>> validFields;
    
    /**
     * Creates new form DeckBuilderFrame
     */
    public DeckBuilderFrame() {
        super();
        cardSheet1 = new CardSheet();
        initComponents();
        setLocationRelativeTo(null);
        jLabel8.setText("Version: " + DeckBuilderFrame.class.getPackage().getImplementationVersion());
        
        deckList.setModel(new DefaultListModel<DeckListItem>());
        deckList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        deckList.setDragEnabled(true);
        deckList.setDropMode(DropMode.INSERT);
        deckList.addMouseListener(new MouseAdapter() {
            
//            @Override
//            public void mouseReleased(MouseEvent e) {
//                if(deckList.locationToIndex(e.getPoint()) != deckList.getSelectedIndex()) {
//                    deckList.setSelectedIndex(deckList.locationToIndex(e.getPoint()));
//                }
//                if(!e.isControlDown() && deckList.getSelectedValue() != null && e.isPopupTrigger()) {
//                    deckItemPopupMenu.show(deckList, e.getX(), e.getY());
//                }
//            }
            
            @Override
            public void mouseClicked(MouseEvent e) {
                if(deckList.locationToIndex(e.getPoint()) != deckList.getSelectedIndex()) {
                    deckList.setSelectedIndex(deckList.locationToIndex(e.getPoint()));
                }
                if(deckList.getSelectedValue() != null) {
                    DeckListItem item = (DeckListItem)deckList.getSelectedValue();
//                    if(e.getClickCount() == 1 && e.getButton() == MouseEvent.BUTTON3 && e.isControlDown()) {
//                        ((DefaultListModel)deckList.getModel()).removeElement(item);
//                    }
//                    else 
                    if(e.getClickCount() % 2 == 0) {
                        try {
                            layerUI.zoomCard(item.getCardData());
                        } catch (IOException ex) {
                            Logger.getLogger(DeckBuilderFrame.class.getName()).log(Level.SEVERE, null, ex);
                        }
//                        if(e.isShiftDown()) {
//                            switch(e.getButton()) {
//                                case MouseEvent.BUTTON1:
//                                    item.incrementStartingCount();
//                                    break;
////                                case MouseEvent.BUTTON3:
////                                    item.decrementStartingCount();
////                                    break;
//                            }
//                        }
//                        else {
//                            switch(e.getButton()) {
//                                case MouseEvent.BUTTON1:
//                                    item.incrementCount();
//                                    break;
////                                case MouseEvent.BUTTON3:
////                                    item.decrementCount();
////                                    if(item.getCount() == 0) {
////                                        ((DefaultListModel)deckList.getModel()).removeElement(item);
////                                    }
////                                    break;
//                            }
//                        }
                    }
//                    deckList.updateUI();
//                    updateCardCount();
                }
            }
        });
        deckList.setTransferHandler(new TransferHandler() {
            @Override
            public boolean canImport(TransferHandler.TransferSupport support) {
                if(!support.isDrop()) {
                    return false;
                }
                
                for(DataFlavor flavor : support.getDataFlavors()) {
                    if(!support.isDataFlavorSupported(flavor)) {
                        return false;
                    }
                }
                return true;
            }
            
            @Override
            public boolean importData(TransferHandler.TransferSupport support) {
                if(support.isDrop()) {
                    try {
                        Transferable tr = support.getTransferable();
                        Object o = null;
                        for(DataFlavor flavor : tr.getTransferDataFlavors()) {
                            if(tr.isDataFlavorSupported(flavor)) {
                                o = tr.getTransferData(flavor);
                            }
                        }
                        
                        if(o instanceof DeckListItem) {
                            DeckListItem item = (DeckListItem)o;
                            int oldIndex = item.getListIndex();
                            
                            DefaultListModel<DeckListItem> model = (DefaultListModel<DeckListItem>)deckList.getModel();
                            int dropIndex = ((JList.DropLocation)support.getDropLocation()).getIndex();
                            
                            switch(support.getDropAction()) {
                                case DnDConstants.ACTION_MOVE:
                                    //remove the original.
                                    model.removeElementAt(oldIndex);
                                    if(oldIndex < dropIndex) {
                                        dropIndex--;
                                    }
                                    //continue to insert the value
                                case DnDConstants.ACTION_COPY:
                                    if(model.contains(item)) {
                                        item = model.get(model.indexOf(item));
                                        item.setCount(item.getCount() + 1);
                                        deckList.updateUI();
                                    }
                                    else {
                                        model.add(dropIndex, item);
                                    }
                                    break;
                                default:
                                    break;
                            }
                            updateCardCount();
                            return true;
                        }
                    } catch (UnsupportedFlavorException | IOException e) {
                        Logger.getLogger(DeckBuilderFrame.class.getName()).log(Level.SEVERE, null, e);
                    }
                }
                return false;
            }
        });
        
        DragSource ds = new DragSource();
        ds.createDefaultDragGestureRecognizer(deckList, DnDConstants.ACTION_MOVE, (DragGestureEvent dge) -> {
            DeckListItem item = (DeckListItem)deckList.getSelectedValue();
            item.setListIndex(deckList.getSelectedIndex());
            dge.startDrag(DragSource.DefaultCopyDrop, new TransferableDeckListItem(item));
        });
        
        // filters
        filters = new ArrayList<>();
        filters.add(new ListFilter(setList, CardData.Field.SET));
        filters.add(new ListFilter(typeList, CardData.Field.TYPE));
        filters.add(new TextFilter(nameFilterField, CardData.Field.NAME));
        filters.add(new TextFilter(gameTextFilterField, CardData.Field.TEXT, CardData.Field.LIGHT_TEXT, CardData.Field.DARK_TEXT));
        filters.add(new TextFilter(loreFilterField, CardData.Field.LORE));
        
        filters.add(new ValidFieldFilter(validFieldCompareFilter1));
        filters.add(new ValidFieldFilter(validFieldCompareFilter2));
        filters.add(new ValidFieldFilter(validFieldCompareFilter3));
        filters.add(new ValidFieldFilter(validFieldCompareFilter4));
        
        filters.add(new SideFilter(sideFilterPanel1, CardData.Field.SIDE));
        
        ActionListener filterActionListener = (ActionEvent e) -> {
            doFilter();
        };
        
        nameFilterField.addActionListener(filterActionListener);
        gameTextFilterField.addActionListener(filterActionListener);
        loreFilterField.addActionListener(filterActionListener);
        filterButton.addActionListener(filterActionListener);
        validFieldCompareFilter1.addValueSelectedActionListener(filterActionListener);
        validFieldCompareFilter2.addValueSelectedActionListener(filterActionListener);
        validFieldCompareFilter3.addValueSelectedActionListener(filterActionListener);
        validFieldCompareFilter4.addValueSelectedActionListener(filterActionListener);
        
        ItemListener filterItemListener = (ItemEvent e) -> {
            doFilter();
        };
        sideFilterPanel1.addItemListener(filterItemListener);
        
        //JList events set up later after they are populated.
        //
        
        // validFields
        Comparator<String> fc = (String s1, String s2) -> {
            try {
                int i1 = Integer.valueOf(s1);
                int i2 = Integer.valueOf(s2);
                
                return i1 - i2;
            }
            catch(NumberFormatException e) {
                return s1.compareTo(s2);
            }
        };
        
        validFields = new EnumMap<>(FilterField.class);
        for(FilterField f : FilterField.values()) {
            validFields.put(f, new TreeSet<>(fc));
        }
        //
    }
    
    private void updateCardCount() {
        int count = 0;
        DefaultListModel<DeckListItem> model = (DefaultListModel<DeckListItem>)deckList.getModel();
        for(int i = 0; i < model.getSize(); i++) {
            DeckListItem item = model.get(i);
            count += item.getCount();
        }
        
        final int count2 = count;
        SwingUtilities.invokeLater(() -> {
            cardCountLabel.setText("Cards in Deck: " + count2);
        });
    }
    
    /**
     * Creates the JLayer object that holds the CardSheet and does the 
     * painting overlays.
     */
    private JLayer<JComponent> createLayer() {
        layerUI = new SheetLayerUI();
        cardSheetLayer = new JLayer<>(cardSheet1, layerUI);
        return cardSheetLayer;
    }

    /**
     * This method is called from within the constructor to initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is always
     * regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        aboutDialog = new javax.swing.JDialog();
        jLabel1 = new javax.swing.JLabel();
        jScrollPane4 = new javax.swing.JScrollPane();
        jTextArea1 = new javax.swing.JTextArea();
        jLabel5 = new javax.swing.JLabel();
        jLabel6 = new javax.swing.JLabel();
        jLabel7 = new javax.swing.JLabel();
        jLabel8 = new javax.swing.JLabel();
        deckItemPopupMenu = new javax.swing.JPopupMenu();
        viewCardMenuItem = new javax.swing.JMenuItem();
        jPanel1 = new javax.swing.JPanel();
        jScrollPane2 = new javax.swing.JScrollPane();
        setList = new javax.swing.JList();
        filterButton = new javax.swing.JButton();
        jScrollPane1 = new javax.swing.JScrollPane();
        typeList = new javax.swing.JList();
        gameTextFilterField = new javax.swing.JTextField();
        jLabel2 = new javax.swing.JLabel();
        nameFilterField = new javax.swing.JTextField();
        jLabel3 = new javax.swing.JLabel();
        clearFilterButton = new javax.swing.JButton();
        jLabel4 = new javax.swing.JLabel();
        loreFilterField = new javax.swing.JTextField();
        validFieldCompareFilter1 = new com.goodjaerb.swccg.ui.ValidFieldFilterPanel();
        validFieldCompareFilter2 = new com.goodjaerb.swccg.ui.ValidFieldFilterPanel();
        validFieldCompareFilter3 = new com.goodjaerb.swccg.ui.ValidFieldFilterPanel();
        validFieldCompareFilter4 = new com.goodjaerb.swccg.ui.ValidFieldFilterPanel();
        jScrollPane3 = new javax.swing.JScrollPane();
        deckList = new javax.swing.JList();
        sideFilterPanel1 = new com.goodjaerb.swccg.ui.SideFilterPanel();
        includeLegacyCheckbox = new javax.swing.JCheckBox();
        cardCountLabel = new javax.swing.JLabel();
        incrementCardButton = new javax.swing.JButton();
        decrementCardButton = new javax.swing.JButton();
        removeCardButton = new javax.swing.JButton();
        menuBar = new javax.swing.JMenuBar();
        fileMenu = new javax.swing.JMenu();
        openDeckMenuItem = new javax.swing.JMenuItem();
        saveDeckMenuItem = new javax.swing.JMenuItem();
        setHolotablePathMenuItem = new javax.swing.JMenuItem();
        jMenuItem1 = new javax.swing.JMenuItem();
        exitMenuItem = new javax.swing.JMenuItem();
        helpMenu = new javax.swing.JMenu();
        aboutMenuItem = new javax.swing.JMenuItem();

        aboutDialog.setTitle("About");
        aboutDialog.setModal(true);
        aboutDialog.setResizable(false);

        jLabel1.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
        jLabel1.setText("SWCCG_DeckBuilder by Steven Kurilovitch (skurilovitch@gmail.com)");

        jTextArea1.setEditable(false);
        jTextArea1.setColumns(20);
        jTextArea1.setFont(new java.awt.Font("Monospaced", 0, 10)); // NOI18N
        jTextArea1.setRows(5);
        jTextArea1.setText("/*\n * Copyright (C) 2014 Steven Kurilovitch\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <http://www.gnu.org/licenses/>.\n */");
        jScrollPane4.setViewportView(jTextArea1);

        jLabel5.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
        jLabel5.setText("Uses Timing Framework library (https://java.net/projects/timingframework)");

        jLabel6.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
        jLabel6.setText(" Data and images are properties of their respective owner.");

        jLabel7.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
        jLabel7.setText("Uses data and images from Holotable. (http://www.holotable.com)");

        jLabel8.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
        jLabel8.setText("Version");

        javax.swing.GroupLayout aboutDialogLayout = new javax.swing.GroupLayout(aboutDialog.getContentPane());
        aboutDialog.getContentPane().setLayout(aboutDialogLayout);
        aboutDialogLayout.setHorizontalGroup(
            aboutDialogLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(aboutDialogLayout.createSequentialGroup()
                .addContainerGap()
                .addGroup(aboutDialogLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jLabel6, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                    .addComponent(jLabel5, javax.swing.GroupLayout.DEFAULT_SIZE, 441, Short.MAX_VALUE)
                    .addComponent(jScrollPane4)
                    .addComponent(jLabel1, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                    .addComponent(jLabel7, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                    .addComponent(jLabel8, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
                .addContainerGap())
        );
        aboutDialogLayout.setVerticalGroup(
            aboutDialogLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(aboutDialogLayout.createSequentialGroup()
                .addContainerGap()
                .addComponent(jLabel1)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                .addComponent(jLabel8)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                .addComponent(jLabel5)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                .addComponent(jLabel7)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                .addComponent(jLabel6)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                .addComponent(jScrollPane4, javax.swing.GroupLayout.PREFERRED_SIZE, 231, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addContainerGap(18, Short.MAX_VALUE))
        );

        viewCardMenuItem.setText("View Card");
        viewCardMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                viewCardMenuItemActionPerformed(evt);
            }
        });
        deckItemPopupMenu.add(viewCardMenuItem);

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        setTitle("DeckBuilder");
        setResizable(false);
        addWindowListener(new java.awt.event.WindowAdapter() {
            public void windowOpened(java.awt.event.WindowEvent evt) {
                formWindowOpened(evt);
            }
        });

        jPanel1.setLayout(new java.awt.BorderLayout());

        jPanel1.add(createLayer(), BorderLayout.CENTER);

        jScrollPane2.setMaximumSize(new java.awt.Dimension(200, 250));
        jScrollPane2.setMinimumSize(new java.awt.Dimension(200, 250));
        jScrollPane2.setPreferredSize(new java.awt.Dimension(200, 250));

        jScrollPane2.setViewportView(setList);

        filterButton.setText("Filter");

        jScrollPane1.setViewportView(typeList);

        jLabel2.setHorizontalAlignment(javax.swing.SwingConstants.RIGHT);
        jLabel2.setText("Text Contains:");

        jLabel3.setHorizontalAlignment(javax.swing.SwingConstants.RIGHT);
        jLabel3.setText("Name Contains:");

        clearFilterButton.setText("Clear");
        clearFilterButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                clearFilterButtonActionPerformed(evt);
            }
        });

        jLabel4.setHorizontalAlignment(javax.swing.SwingConstants.RIGHT);
        jLabel4.setText("Lore Contains:");

        jScrollPane3.setViewportView(deckList);

        includeLegacyCheckbox.setText("Include Legacy Virtual Sets");
        includeLegacyCheckbox.addItemListener(new java.awt.event.ItemListener() {
            public void itemStateChanged(java.awt.event.ItemEvent evt) {
                includeLegacyCheckboxItemStateChanged(evt);
            }
        });

        cardCountLabel.setText("Cards in Deck: 0");

        incrementCardButton.setText("+");
        incrementCardButton.setToolTipText("Increment selected card. Hold Shift to increment starting count.");
        incrementCardButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                incrementCardButtonActionPerformed(evt);
            }
        });

        decrementCardButton.setText("-");
        decrementCardButton.setToolTipText("Decrement selected card. Hold Shift to decrement starting count.");
        decrementCardButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                decrementCardButtonActionPerformed(evt);
            }
        });

        removeCardButton.setText("Remove");
        removeCardButton.setToolTipText("Remove the selected card from the deck entirely.");
        removeCardButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                removeCardButtonActionPerformed(evt);
            }
        });

        fileMenu.setMnemonic('f');
        fileMenu.setText("File");

        openDeckMenuItem.setText("Open Deck...");
        openDeckMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                openDeckMenuItemActionPerformed(evt);
            }
        });
        fileMenu.add(openDeckMenuItem);

        saveDeckMenuItem.setText("Save Deck...");
        saveDeckMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                saveDeckMenuItemActionPerformed(evt);
            }
        });
        fileMenu.add(saveDeckMenuItem);

        setHolotablePathMenuItem.setText("Set Holotable Path...");
        setHolotablePathMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                setHolotablePathMenuItemActionPerformed(evt);
            }
        });
        fileMenu.add(setHolotablePathMenuItem);

        jMenuItem1.setText("Clear Preferences");
        jMenuItem1.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jMenuItem1ActionPerformed(evt);
            }
        });
        fileMenu.add(jMenuItem1);

        exitMenuItem.setMnemonic('x');
        exitMenuItem.setText("Exit");
        exitMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exitMenuItemActionPerformed(evt);
            }
        });
        fileMenu.add(exitMenuItem);

        menuBar.add(fileMenu);

        helpMenu.setMnemonic('h');
        helpMenu.setText("Help");

        aboutMenuItem.setMnemonic('a');
        aboutMenuItem.setText("About");
        aboutMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                aboutMenuItemActionPerformed(evt);
            }
        });
        helpMenu.add(aboutMenuItem);

        menuBar.add(helpMenu);

        setJMenuBar(menuBar);

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addComponent(jPanel1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addGap(0, 0, Short.MAX_VALUE))
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addComponent(jScrollPane2, javax.swing.GroupLayout.PREFERRED_SIZE, 192, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 192, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false)
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                            .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                                .addComponent(validFieldCompareFilter1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                                .addComponent(validFieldCompareFilter2, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                            .addComponent(validFieldCompareFilter3, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                            .addComponent(validFieldCompareFilter4, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                        .addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup()
                            .addComponent(includeLegacyCheckbox, javax.swing.GroupLayout.PREFERRED_SIZE, 193, javax.swing.GroupLayout.PREFERRED_SIZE)
                            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                            .addComponent(cardCountLabel)))
                    .addGroup(layout.createSequentialGroup()
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                            .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false)
                                .addComponent(jLabel2, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                                .addComponent(jLabel3, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.PREFERRED_SIZE, 84, javax.swing.GroupLayout.PREFERRED_SIZE))
                            .addComponent(jLabel4, javax.swing.GroupLayout.PREFERRED_SIZE, 84, javax.swing.GroupLayout.PREFERRED_SIZE))
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                            .addComponent(loreFilterField, javax.swing.GroupLayout.PREFERRED_SIZE, 208, javax.swing.GroupLayout.PREFERRED_SIZE)
                            .addComponent(gameTextFilterField, javax.swing.GroupLayout.PREFERRED_SIZE, 208, javax.swing.GroupLayout.PREFERRED_SIZE)
                            .addComponent(nameFilterField, javax.swing.GroupLayout.PREFERRED_SIZE, 208, javax.swing.GroupLayout.PREFERRED_SIZE))
                        .addGap(33, 33, 33)
                        .addComponent(sideFilterPanel1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                    .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
                        .addComponent(clearFilterButton)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(filterButton)))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addGroup(layout.createSequentialGroup()
                        .addComponent(incrementCardButton)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(decrementCardButton, javax.swing.GroupLayout.PREFERRED_SIZE, 32, javax.swing.GroupLayout.PREFERRED_SIZE)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(removeCardButton, javax.swing.GroupLayout.DEFAULT_SIZE, 112, Short.MAX_VALUE))
                    .addComponent(jScrollPane3, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE))
                .addContainerGap())
        );

        layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {decrementCardButton, incrementCardButton});

        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addComponent(jPanel1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                            .addGroup(layout.createSequentialGroup()
                                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                                    .addGroup(layout.createSequentialGroup()
                                        .addGap(14, 14, 14)
                                        .addComponent(sideFilterPanel1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                                    .addGroup(layout.createSequentialGroup()
                                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                                            .addComponent(jLabel3)
                                            .addComponent(nameFilterField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                                        .addGap(6, 6, 6)
                                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                                            .addComponent(gameTextFilterField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                                            .addComponent(jLabel2))
                                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                                            .addComponent(loreFilterField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                                            .addComponent(jLabel4))))
                                .addGap(18, 18, 18)
                                .addComponent(validFieldCompareFilter1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                                .addComponent(validFieldCompareFilter2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                                .addComponent(validFieldCompareFilter3, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                                .addComponent(validFieldCompareFilter4, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                                    .addComponent(filterButton)
                                    .addComponent(clearFilterButton))
                                .addGap(0, 0, Short.MAX_VALUE))
                            .addComponent(jScrollPane3))
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                            .addComponent(includeLegacyCheckbox)
                            .addComponent(cardCountLabel)
                            .addComponent(incrementCardButton)
                            .addComponent(decrementCardButton)
                            .addComponent(removeCardButton)))
                    .addComponent(jScrollPane2, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 256, Short.MAX_VALUE)
                    .addComponent(jScrollPane1, javax.swing.GroupLayout.Alignment.TRAILING))
                .addContainerGap())
        );

        pack();
    }// </editor-fold>//GEN-END:initComponents

    private void exitMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exitMenuItemActionPerformed
        this.dispose();
        System.exit(0); // I'm not a big fan but the dispose wasn't cutting it.
    }//GEN-LAST:event_exitMenuItemActionPerformed

    private void aboutMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_aboutMenuItemActionPerformed
        aboutDialog.pack();
        aboutDialog.setLocationRelativeTo(this);
        aboutDialog.setVisible(true);
    }//GEN-LAST:event_aboutMenuItemActionPerformed

    private void setHolotablePathMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_setHolotablePathMenuItemActionPerformed
        JFileChooser chooser = new JFileChooser();
        chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
        
        int result = chooser.showOpenDialog(this);
        switch(result) {
            case JFileChooser.APPROVE_OPTION:
                holotablePath = chooser.getSelectedFile().getPath();
                if(new File(holotablePath).exists()) {
                    try {
                        readCardData();
// DEBUG
//                        validFields.values().stream().forEach((Set<String> ts) -> {
//                            System.out.println(ts);
//                        });
                    } catch (IOException ex) {
                        Logger.getLogger(DeckBuilderFrame.class.getName()).log(Level.SEVERE, null, ex);
                    }
                    Preferences.userNodeForPackage(DeckBuilderFrame.class).put(HOLOTABLE_PATH_PREF, holotablePath);
                }
                else {
                    JOptionPane.showMessageDialog(this, "Images path not found.");
                }
                break;
            default:
                // failure.
                holotablePath = null;
                break;
        }
    }//GEN-LAST:event_setHolotablePathMenuItemActionPerformed

    private void formWindowOpened(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_formWindowOpened
        holotablePath = Preferences.userNodeForPackage(DeckBuilderFrame.class).get(HOLOTABLE_PATH_PREF, null);
        if(holotablePath != null) {
            if(new File(holotablePath).exists()) {
                try {
                    readCardData();
// DEBUG
//                    for(FilterField f : FilterField.values()) {
//                        System.out.println(f + ": " + validFields.get(f));
//                    }
                    
                    //
                    DefaultListModel<String> m = new DefaultListModel<>();
                    validFields.get(FilterField.SET).stream().forEach((s) -> {
                        m.addElement(s);
                    });
                    setList.setModel(m);
                    //
                    DefaultListModel<String> m2 = new DefaultListModel<>();
                    validFields.get(FilterField.TYPE).stream().forEach((s) -> {
                        m2.addElement(s);
                    });
                    typeList.setModel(m2);

                    ListSelectionListener filterListSelectionListener = (ListSelectionEvent e) -> {
                        doFilter();
                    };
                    setList.setSelectionInterval(0, setList.getModel().getSize() - 1);
                    typeList.setSelectionInterval(0, typeList.getModel().getSize() - 1);
                    setList.addListSelectionListener(filterListSelectionListener);
                    typeList.addListSelectionListener(filterListSelectionListener);
                    
                    //
                    validFieldCompareFilter1.setValidFields(validFields);
                    validFieldCompareFilter2.setValidFields(validFields);
                    validFieldCompareFilter3.setValidFields(validFields);
                    validFieldCompareFilter4.setValidFields(validFields);
                    //
                    doFilter();
                } catch (IOException ex) {
                    Logger.getLogger(DeckBuilderFrame.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
            else {
                holotablePath = null;
                JOptionPane.showMessageDialog(this, "Images path not found.");
            }
        }
    }//GEN-LAST:event_formWindowOpened

    private void jMenuItem1ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jMenuItem1ActionPerformed
        try {
            Preferences.userNodeForPackage(DeckBuilderFrame.class).clear();
            cardSheet1.clearSheet();
        } catch (BackingStoreException | IOException ex) {
            Logger.getLogger(DeckBuilderFrame.class.getName()).log(Level.SEVERE, null, ex);
        }
    }//GEN-LAST:event_jMenuItem1ActionPerformed

    private void clearFilterButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_clearFilterButtonActionPerformed
        setList.setSelectionInterval(0, setList.getModel().getSize() - 1);
        typeList.setSelectionInterval(0, typeList.getModel().getSize() - 1);
        nameFilterField.setText("");
        gameTextFilterField.setText("");
        loreFilterField.setText("");
        
        validFieldCompareFilter1.clear();
        validFieldCompareFilter2.clear();
        validFieldCompareFilter3.clear();
        validFieldCompareFilter4.clear();
        
        sideFilterPanel1.setSelected(true, true);
        
        doFilter();
    }//GEN-LAST:event_clearFilterButtonActionPerformed

    private void saveDeckMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveDeckMenuItemActionPerformed
        if(deckList.getModel().getSize() == 0) {
            JOptionPane.showMessageDialog(this, "There are no cards in your deck!", "Error", JOptionPane.ERROR_MESSAGE);
            return;
        }
        
        DefaultListModel<DeckListItem> model = (DefaultListModel<DeckListItem>)deckList.getModel();
        CardData.Side side = model.getElementAt(0).getCardData().getSide();
        for(int i = 1; i < model.getSize(); i++) {
            if(model.getElementAt(i).getCardData().getSide() != side) {
                JOptionPane.showMessageDialog(this, "Deck contains cards from both Light and Dark sides!", "Error", JOptionPane.ERROR_MESSAGE);
                return;
            }
        }
        
        JFileChooser chooser = new JFileChooser(Preferences.userNodeForPackage(DeckBuilderFrame.class).get(PREVIOUS_OPEN_PATH_PREF, null));
        chooser.setAcceptAllFileFilterUsed(false);
        chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
        chooser.setFileFilter(new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                return pathname.isDirectory() || pathname.getName().endsWith(".htd");
            }

            @Override
            public String getDescription() {
                return "Holotable Deck (.htd)";
            }
        });
        
        int result = chooser.showSaveDialog(this);
        if(result == JFileChooser.APPROVE_OPTION) {
            File saveFile = chooser.getSelectedFile();
            if(!saveFile.getName().endsWith(".htd")) {
                saveFile = new File(saveFile.getAbsolutePath() + ".htd");
            }
            Preferences.userNodeForPackage(DeckBuilderFrame.class).put(PREVIOUS_OPEN_PATH_PREF, saveFile.getParent());
            
            try(PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(saveFile)))) {
                writer.println("ver 2");
                if(((DeckListItem)((DefaultListModel)deckList.getModel()).get(0)).getCardData().getSide() == CardData.Side.LIGHT) {
                    writer.println("back reb.gif");
                }
                else {
                    writer.println("back imp.gif");
                }
                for(Object o : ((DefaultListModel)deckList.getModel()).toArray()) {
                    DeckListItem item = (DeckListItem)o;
                    String line = item.getCardData().get(CardData.Field.HOLOTABLE_CARD_PATH) + " " + item.getCount();
                    if(item.isStarting()) {
                        line += " 1 " + item.getStartingCount();
                    }
                    writer.println(line);
                }
            } catch (IOException ex) {
                Logger.getLogger(DeckBuilderFrame.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }//GEN-LAST:event_saveDeckMenuItemActionPerformed

    private void includeLegacyCheckboxItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_includeLegacyCheckboxItemStateChanged
        Set<String> sets = new TreeSet<>();
        switch(evt.getStateChange()) {
            case ItemEvent.SELECTED:
                Collections.addAll(sets, validFields.get(FilterField.SET).toArray(new String[] {}));
                Collections.addAll(sets, validFields.get(FilterField.LEGACY_SET).toArray(new String[] {}));
                break;
            case ItemEvent.DESELECTED:
                Collections.addAll(sets, validFields.get(FilterField.SET).toArray(new String[] {}));
                break;
        }
        DefaultListModel<String> setsModel = new DefaultListModel<>();
        sets.stream().forEach((s) -> {
            setsModel.addElement(s);
        });
        setList.setModel(setsModel);
        setList.setSelectionInterval(0, setsModel.size() - 1);
        doFilter();
    }//GEN-LAST:event_includeLegacyCheckboxItemStateChanged

    private void openDeckMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_openDeckMenuItemActionPerformed
        if(deckList.getModel().getSize() > 0) {
            int result = JOptionPane.showConfirmDialog(this, "Any unsaved changes will be lost. Do you want to open a new deck?", "Open Deck", JOptionPane.YES_NO_OPTION);
            if(result == JOptionPane.NO_OPTION) {
                return;
            }
        }
        
        JFileChooser chooser = new JFileChooser(Preferences.userNodeForPackage(DeckBuilderFrame.class).get(PREVIOUS_OPEN_PATH_PREF, null));
        chooser.setAcceptAllFileFilterUsed(false);
        chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
        chooser.setFileFilter(new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                return pathname.isDirectory() || pathname.getName().endsWith(".htd");
            }

            @Override
            public String getDescription() {
                return "Holotable Deck (.htd)";
            }
        });
        int result = chooser.showOpenDialog(this);
        if(result == JFileChooser.APPROVE_OPTION) {
            deckList.setModel(new DefaultListModel<DeckListItem>());
            
            File deckFile = chooser.getSelectedFile();
            Preferences.userNodeForPackage(DeckBuilderFrame.class).put(PREVIOUS_OPEN_PATH_PREF, deckFile.getParent());
                    
            Pattern linePattern = Pattern.compile("^(card \".*?\") (\\d+)( \\d+ (\\d+))?$", Pattern.CASE_INSENSITIVE);
            Matcher lineMatcher;
            
            try(BufferedReader reader = new BufferedReader(new FileReader(deckFile))) {
                String line;
                while((line = reader.readLine()) != null) {
                    lineMatcher = linePattern.matcher(line);
                    if(lineMatcher.matches()) {
                        CardData cd = cardSheet1.getCard(lineMatcher.group(1));
                        if(cd != null) {
                            DeckListItem item = new DeckListItem(cd);
                            item.setCount(Integer.parseInt(lineMatcher.group(2)));
                            if(lineMatcher.group(4) != null) {
                                item.setStartingCount(Integer.parseInt(lineMatcher.group(4)));
                            }
                            ((DefaultListModel)deckList.getModel()).addElement(item);
                        }
                    }
                }
                updateCardCount();
            }
            catch (IOException ex) {
                Logger.getLogger(DeckBuilderFrame.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }//GEN-LAST:event_openDeckMenuItemActionPerformed

    private void incrementCardButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_incrementCardButtonActionPerformed
        DeckListItem item = (DeckListItem)deckList.getSelectedValue();
        if(item != null) {
            if((evt.getModifiers() & ActionEvent.SHIFT_MASK) == ActionEvent.SHIFT_MASK) {
                item.incrementStartingCount();
            }
            else {
                item.incrementCount();
            }
            deckList.updateUI();
            updateCardCount();
        }
    }//GEN-LAST:event_incrementCardButtonActionPerformed

    private void decrementCardButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_decrementCardButtonActionPerformed
        DeckListItem item = (DeckListItem)deckList.getSelectedValue();
        if(item != null) {
            if((evt.getModifiers() & ActionEvent.SHIFT_MASK) == ActionEvent.SHIFT_MASK) {
                item.decrementStartingCount();
            }
            else {
                item.decrementCount();
                if(item.getCount() == 0) {
                    ((DefaultListModel)deckList.getModel()).removeElement(item);
                }
            }
            deckList.updateUI();
            updateCardCount();
        }
    }//GEN-LAST:event_decrementCardButtonActionPerformed

    private void removeCardButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_removeCardButtonActionPerformed
        DeckListItem item = (DeckListItem)deckList.getSelectedValue();
        if(item != null) {
            int index = deckList.getSelectedIndex();
            ((DefaultListModel)deckList.getModel()).removeElement(item);
            
            if(index == ((DefaultListModel)deckList.getModel()).getSize()) {
                index--;
            }
            deckList.setSelectedIndex(index);
            deckList.updateUI();
            updateCardCount();
        }
    }//GEN-LAST:event_removeCardButtonActionPerformed

    private void viewCardMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_viewCardMenuItemActionPerformed
        DeckListItem item = (DeckListItem)deckList.getSelectedValue();
        try {
            layerUI.zoomCard(item.getCardData());
        } catch (IOException ex) {
            Logger.getLogger(DeckBuilderFrame.class.getName()).log(Level.SEVERE, null, ex);
        }
    }//GEN-LAST:event_viewCardMenuItemActionPerformed

    private void doFilter() {
        cardSheet1.filter(filters);
        //
        try {
            cardSheet1.resetSheet();
        } catch (IOException ex) {
            Logger.getLogger(DeckBuilderFrame.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
    
    private void readCardData() throws FileNotFoundException, IOException {
        cardSheet1.clearSheet();
        readCDF("lightside.cdf", CardData.Side.LIGHT);
        readCDF("darkside.cdf", CardData.Side.DARK);
        cardSheet1.resetSheet();
    }
    
    private void readCDF(String fileName, CardData.Side side) throws FileNotFoundException, IOException {
        File cdf = new File(holotablePath + File.separator + fileName);
        if(cdf.exists()) {
            /**
             * Reading someone else's data is never easy and there are a number of formatting inconsistencies to deal with.
             * I'm not a regular expression wiz but I get by. The rest of it I do old-school string parsing!
             */
            Pattern linePattern = Pattern.compile("^(card \"(.*)\") \"(.*)\"$", Pattern.CASE_INSENSITIVE);
            Matcher lineMatcher;
            
            Pattern nameAndDestinyPattern = Pattern.compile("^(([\\W]{0,6})(.*)) *(?:\\(\\w\\))?\\((.*)\\)$", Pattern.CASE_INSENSITIVE);
            Matcher nameAndDestinyMatcher;
            
            Pattern sideTypeAndRarityPattern = Pattern.compile("^(Light|Dark).*\\[(.*)\\]$", Pattern.CASE_INSENSITIVE);
            Matcher sideTypeAndRarityMatcher;
            
            Pattern powerAndDefensePattern = Pattern.compile("^ ?Power:? (.*?) +((Ability|Armor|Maneuver|Manuever|FLIGHT|CLAWS|SWARM|CAMOUFLAGE|SCURRY|HIDE|SLITHER|JUMP|BARK|TENTACLES|HUMP|MOURNFUL HOWL|VICIOUS HOWL|ARMORED HIDE|CARAPACE|FANGS):? (.*?))? +(Dark Jedi Master|Dark Jedi|Jedi Master|Jedi Knight|Force Sensitive|Force-Sensitive|Force-Sensative|Force-Attuned|.* Droid|Astromech & Protocol)? *((Armor|Politics|Landspeed|Hyperspeed):? ?(.*?) ?)?", Pattern.CASE_INSENSITIVE);
            Matcher powerAndDefenseMatcher;
            
            Pattern deployAndForfeitPattern = Pattern.compile("^ ?Deploy:? ?([\\d|\\w]*)?.*(Forfeit|Forfiet):? (.*?) ?(Carbon-Frozen)?$", Pattern.CASE_INSENSITIVE);
            Matcher deployAndForfeitMatcher;
            
            Pattern parsecPattern = Pattern.compile(".*?Parsec:? (\\d{1,2}|X).*?");
            Matcher parsecMatcher;
                    
            Pattern cardTypePattern = Pattern.compile("^\\s*\\[(.*)\\]\\s*$");
            Matcher cardTypeMatcher;
            
            Pattern starshipAndVehicleSubtypePattern = Pattern.compile(".*?(Light|Dark).*?(Capital:|Starfighter:|Squadron:|Vehicle:|Combat:|Creature:|Shuttle:|Transport:) ?(.*) ?\\[\\w{1,2}\\].*?");
            Matcher starshipAndVehicleSubtypeMatcher;
            
            CardData.Type cardType = null;
            
            try (BufferedReader reader = new BufferedReader(new FileReader(cdf))) {
                String line;
                while((line = reader.readLine()) != null) {
                    cardTypeMatcher = cardTypePattern.matcher(line);
                    if(cardTypeMatcher.matches()) {
                        String typeStr = cardTypeMatcher.group(1);
                        typeStr = typeStr.replace("'", "").replaceAll("( - |\\s|\\W)", "_").toUpperCase();
                        cardType = CardData.Type.valueOf(typeStr);
                        addValidField(FilterField.TYPE, cardType.toString());
                    }
                    else {
                        lineMatcher = linePattern.matcher(line);
                        if(lineMatcher.matches()) {
                            CardData cd = new CardData(side, cardType);

                            // image paths
                            String holotableCardPath = lineMatcher.group(1);
                            cd.set(CardData.Field.HOLOTABLE_CARD_PATH, holotableCardPath);
                            
                            String thumbPath = lineMatcher.group(2);

                            String[] pathParts = thumbPath.split("t_");
                            if(pathParts[0].startsWith("/TWOSIDED")) {
                                String firstPart = pathParts[0].replace("/TWOSIDED", "");

                                String[] twoNames = pathParts[1].split("/");
                                String card1Path = holotablePath + File.separator + "cards" + firstPart + "large" + File.separator + twoNames[0] + ".gif";
                                String card2Path = holotablePath + File.separator + "cards" + firstPart + "large" + File.separator + twoNames[1] + ".gif";

                                cd.setTwoSided(true);
                                cd.setImagePath(card1Path);
                                cd.setBackImagePath(card2Path);
                            }
                            else {
                                String imagePath = holotablePath + File.separator + "cards" + pathParts[0] + "large" + File.separator + pathParts[1] + ".gif";//thumbMatcher.group(2) + ".gif";
                                cd.setImagePath(imagePath);
                            }
                            //
                            
                            if(new File(cd.getImagePath()).exists()) {
                                // image exists so grab card data
                                String[] parts = getNextParts(lineMatcher.group(3));
                                nameAndDestinyMatcher = nameAndDestinyPattern.matcher(parts[0]);
                                if(nameAndDestinyMatcher.matches()) {
                                    cd.set(CardData.Field.TITLE, nameAndDestinyMatcher.group(1).replaceAll("<>", "◊").trim());
                                    cd.set(CardData.Field.NAME, nameAndDestinyMatcher.group(3).trim());

                                    if(nameAndDestinyMatcher.group(2).length() > 0) {
                                        String ch = cd.get(CardData.Field.TITLE).substring(0, 1);
                                        cd.set(CardData.Field.TITLE, cd.get(CardData.Field.TITLE).replaceAll(ch, "•"));
                                    }
                                    cd.set(CardData.Field.DESTINY, nameAndDestinyMatcher.group(4));
                                    addValidField(FilterField.DESTINY, cd.get(CardData.Field.DESTINY));
                                }

                                parts = getNextParts(parts[1]);
                                sideTypeAndRarityMatcher = sideTypeAndRarityPattern.matcher(parts[0]);
                                if(sideTypeAndRarityMatcher.matches()) {
                                    cd.set(CardData.Field.RARITY, sideTypeAndRarityMatcher.group(2));
                                }

                                parts = getNextParts(parts[1]);
                                cd.set(CardData.Field.SET, parts[0].substring(5));

                                if(cd.isLegacy()) {
                                    addValidField(FilterField.LEGACY_SET, cd.get(CardData.Field.SET));
                                }
                                else {
                                    addValidField(FilterField.SET, cd.get(CardData.Field.SET));
                                }
                                
                                String typeStr = cardType.name();
                                if((typeStr.contains("CHARACTER") || typeStr.contains("CREATURE") || typeStr.contains("VEHICLE") || typeStr.contains("STARSHIP")) && !typeStr.contains("WEAPON")) {
                                    parts = getNextParts(parts[1]);

                                    powerAndDefenseMatcher = powerAndDefensePattern.matcher(parts[0]);
                                    if(powerAndDefenseMatcher.matches()) {
                                        cd.set(CardData.Field.POWER, powerAndDefenseMatcher.group(1));

                                        addValidField(FilterField.POWER, cd.get(CardData.Field.POWER));

                                        String defenseLabel = powerAndDefenseMatcher.group(3);
                                        if(defenseLabel != null) {
                                            switch (defenseLabel) {
                                                case "Ability":
                                                    cd.set(CardData.Field.ABILITY, powerAndDefenseMatcher.group(4));
                                                    addValidField(FilterField.ABILITY, cd.get(CardData.Field.ABILITY));
                                                    break;
                                                case "Manuever":
                                                    defenseLabel = "Maneuver";
                                                    break;
                                            }
                                            cd.set(CardData.Field.DEFENSE_LABEL, defenseLabel);
                                            cd.set(CardData.Field.DEFENSE, powerAndDefenseMatcher.group(4));

                                            addValidField(FilterField.DEFENSE_LABEL, cd.get(CardData.Field.DEFENSE_LABEL));
                                            addValidField(FilterField.DEFENSE, cd.get(CardData.Field.DEFENSE));
                                        }

                                        String m = powerAndDefenseMatcher.group(5);
                                        if(m != null && !m.equals("")) {
                                            switch (m) {
                                                case "Force Sensitive":
                                                case "Force-Sensative":
                                                    m = "Force-Sensitive";
                                                    break;
                                                case "DARK JEDI MASTER":
                                                    m = "Dark Jedi Master";
                                                    break;
                                            }
                                            cd.set(CardData.Field.SUBTYPE, m);
                                            cd.set(CardData.Field.CHARACTER_SUBTYPE, m);
                                            addValidField(FilterField.SUBTYPE, m);
                                            addValidField(FilterField.CHARACTER_SUBTYPE, m);
                                        }

                                        String armorOrSpeedLabel = powerAndDefenseMatcher.group(7);
                                        if(armorOrSpeedLabel != null) {
                                            switch (armorOrSpeedLabel) {
                                                case "Armor":
                                                    cd.set(CardData.Field.DEFENSE_LABEL, armorOrSpeedLabel);
                                                    cd.set(CardData.Field.DEFENSE, powerAndDefenseMatcher.group(8));
                                                    addValidField(FilterField.DEFENSE_LABEL, cd.get(CardData.Field.DEFENSE_LABEL));
                                                    addValidField(FilterField.DEFENSE, cd.get(CardData.Field.DEFENSE));
                                                    break;
                                                case "Politics":
                                                    cd.set(CardData.Field.POLITICS, powerAndDefenseMatcher.group(8));
                                                    addValidField(FilterField.POLITICS, cd.get(CardData.Field.POLITICS));
                                                    break;
                                                default:
                                                    cd.set(CardData.Field.SPEED_LABEL, armorOrSpeedLabel);
                                                    cd.set(CardData.Field.SPEED, powerAndDefenseMatcher.group(8));
                                                    addValidField(FilterField.SPEED_LABEL, cd.get(CardData.Field.SPEED_LABEL));
                                                    addValidField(FilterField.SPEED, cd.get(CardData.Field.SPEED));
                                                    break;
                                            }
                                        }
                                    }

                                    parts = getNextParts(parts[1]);
                                    deployAndForfeitMatcher = deployAndForfeitPattern.matcher(parts[0]);
                                    if(deployAndForfeitMatcher.matches()) {
                                        cd.set(CardData.Field.DEPLOY, deployAndForfeitMatcher.group(1));
                                        cd.set(CardData.Field.FORFEIT, deployAndForfeitMatcher.group(3));

                                        addValidField(FilterField.DEPLOY, cd.get(CardData.Field.DEPLOY));
                                        addValidField(FilterField.FORFEIT, cd.get(CardData.Field.FORFEIT));
                                        // Jabba's Prize has it's qualifier after the forfeit instead of ability for some reason
                                        String m = deployAndForfeitMatcher.group(4);
                                        if(m != null && !m.equals("")) {
                                            m = m.trim();
                                            cd.set(CardData.Field.SUBTYPE, m);
                                            addValidField(FilterField.SUBTYPE, m);
                                            addValidField(FilterField.CHARACTER_SUBTYPE, m);
                                        }
                                    }
                                }

                                if(typeStr.contains("WEAPON")) {
                                    parts = getNextParts(parts[1]);

                                    if(parts[0].startsWith("Icons:")) {
                                        cd.setIcons(parseIcons(parts[0].substring(6)));
                                        parts = getNextParts(parts[1]);
                                    }

                                    if(typeStr.contains("ARTILLERY")) {
                                        deployAndForfeitMatcher = deployAndForfeitPattern.matcher(parts[0]);
                                        if(deployAndForfeitMatcher.matches()) {
                                            cd.set(CardData.Field.DEPLOY, deployAndForfeitMatcher.group(1));
                                            cd.set(CardData.Field.FORFEIT, deployAndForfeitMatcher.group(3));

                                            addValidField(FilterField.DEPLOY, cd.get(CardData.Field.DEPLOY));
                                            addValidField(FilterField.FORFEIT, cd.get(CardData.Field.FORFEIT));
                                        }
                                    }

                                    // there's a weapon with no lore in the CDF so i have to grab the text here before the next call to getNextParts skips past it.
                                    if(parts[0].startsWith("Text:")) {
                                        cd.set(CardData.Field.TEXT, parts[0].substring(6).replace("\\n", " ") + " " + parts[1].replace("\\n", " "));
                                    }
                                }

                                //parsec is just in the middle of nowhere so i'll grab it here.
                                //also because of the one parsec in the middle of the icons i have to do this.
                                //now that i've grabbed this value like this i realize maybe this could've been the
                                //pattern for grabbing all of them instead of the weird iteration i was using.
                                if(typeStr.contains("LOCATION_SYSTEM")) {
                                    parsecMatcher = parsecPattern.matcher(line);
                                    if(parsecMatcher.matches()) {
                                        cd.set(CardData.Field.PARSEC, parsecMatcher.group(1));
                                        addValidField(FilterField.PARSEC, cd.get(CardData.Field.PARSEC));
                                    }
                                }

                                if(!typeStr.contains("WEAPON") && (typeStr.contains("STARSHIP") || typeStr.contains("VEHICLE"))) {
                                    starshipAndVehicleSubtypeMatcher = starshipAndVehicleSubtypePattern.matcher(line);
                                    if(starshipAndVehicleSubtypeMatcher.matches()) {
                                        String subtype = starshipAndVehicleSubtypeMatcher.group(3).trim();
                                        if(subtype.equals("TEI/LN")) {
                                            subtype = "TIE/LN";
                                        }

                                        cd.set(CardData.Field.SUBTYPE, subtype);
                                        addValidField(FilterField.SUBTYPE, subtype);
                                        if(typeStr.contains("CAPITAL")) {
                                            cd.set(CardData.Field.CAPITAL_SUBTYPE, subtype);
                                            addValidField(FilterField.CAPITAL_SUBTYPE, subtype);
                                        }
                                        else if(typeStr.contains("SQUADRON")) {
                                            cd.set(CardData.Field.SQUADRON_SUBTYPE, subtype);
                                            addValidField(FilterField.SQUADRON_SUBTYPE, subtype);
                                        }
                                        else if(typeStr.contains("STARFIGHTER")) {
                                            cd.set(CardData.Field.STARFIGHTER_SUBTYPE, subtype);
                                            addValidField(FilterField.STARFIGHTER_SUBTYPE, subtype);
                                        }
                                        else {
                                            cd.set(CardData.Field.VEHICLE_SUBTYPE, subtype);
                                            addValidField(FilterField.VEHICLE_SUBTYPE, subtype);
                                        }
                                    }
                                }

                                if(typeStr.contains("OBJECTIVE")) {
                                    parts = getNextParts(parts[1]);

                                    if(parts[0].startsWith("Icons:")) {
                                        cd.setIcons(parseIcons(parts[0].substring(6)));
                                        parts = getNextParts(parts[1]);
                                    }
                                    cd.set(CardData.Field.TEXT, parts[0].replaceAll("(\\\\n)+", " ") + " " + parts[1].replaceAll("(\\\\n)+", " "));
                                }
                                else {
                                    boolean gotLore = false;
                                    boolean done = false;
                                    parts = getNextParts(parts[1]);
                                    while(!done && !(parts[0].equals("") && parts[1].equals(""))) {
                                        if(parts[0].startsWith("Icons:")) {
                                            cd.setIcons(parseIcons(parts[0].substring(6)));
                                        }
                                        else if(parts[0].startsWith("Lore:")) {
                                            if(parts[0].length() > 5) {
                                                cd.set(CardData.Field.LORE, parts[0].substring(6));
                                            }
                                            gotLore = true;
                                        }
                                        else if(parts[0].startsWith("Text:")) {
                                            if(parts[0].length() == 5) {
                                                parseText(cd, parts[1].replace("\\n", " "));
                                            }
                                            else {
                                                parseText(cd, parts[0].substring(6).replace("\\n", " ") + " " + parts[1].replace("\\n", " "));
                                            }
                                            done = true;
                                        }
                                        else if(gotLore) {
                                            // There's a line in the CDF that doesn't say "TEXT: " so I have to just grab what's after the lore.
                                            parseText(cd, parts[0].replace("\\n", " ") + " " + parts[1].replace("\\n", " "));
                                            done = true;
                                        }
                                        parts = getNextParts(parts[1]);
                                    }
                                }
                                //

                                cardSheet1.addCard(cd);
                            }
                        }
                    }
                }
            }
        }
    }
    
    private void addValidField(FilterField f, String v) {
        if(v != null && !v.equals("")) {
            validFields.get(f).add(v);
        }
    }
    
    /**
     * Parses text where the cardtype might be a location so that it finds the extra info and separates it.
     * @param cd
     * @param text 
     */
    private void parseText(CardData cd, String text) {
        if(!cd.get(CardData.Field.TYPE).contains("LOCATION")) {
            cd.set(CardData.Field.TEXT, text);
        }
        else {
            Matcher locationMatcher = Pattern.compile("^\\s*(LIGHT|DARK) ?\\((\\d)\\):? ?(.*?)(LIGHT|DARK) ?\\((\\d)\\):? ?(.*)$", Pattern.CASE_INSENSITIVE).matcher(text);
            
            if(locationMatcher.matches()) {
                if(locationMatcher.group(1).toUpperCase().equals("LIGHT")) {
                    cd.set(CardData.Field.LIGHT_FORCE, locationMatcher.group(2));
                    cd.set(CardData.Field.LIGHT_TEXT, locationMatcher.group(3));
                    
                    cd.set(CardData.Field.DARK_FORCE, locationMatcher.group(5));
                    cd.set(CardData.Field.DARK_TEXT, locationMatcher.group(6));
                }
                else {
                    cd.set(CardData.Field.DARK_FORCE, locationMatcher.group(2));
                    cd.set(CardData.Field.DARK_TEXT, locationMatcher.group(3));
                    
                    cd.set(CardData.Field.LIGHT_FORCE, locationMatcher.group(5));
                    cd.set(CardData.Field.LIGHT_TEXT, locationMatcher.group(6));
                }
            }
        }
    }
    
    private List<CardData.Icon> parseIcons(String iconStr) {
        List<CardData.Icon> icons = new ArrayList<>();
        
        String[] split = iconStr.split(",");
        for(String p : split) {
            // some leading spaces to deal with.. some Warrior x2 and Pilot x3 type things to filter in to just WARRIOR or PILOT icons.
            // and a parsec that slipped into the icons.
            if(!p.contains("Parsec")) {
                String n = p.replace("Astromech", "Nav Computer").trim();
                n = n.replace("Anakin Pilot", "Pilot");
                n = n.replace("Presence Droid", "Presence");
                n = n.replace("Somp Link", "Scomp Link");
                n = n.replaceAll("(^\\s*|'|\\s*?x[\\d])", "").replaceAll("(\\s|\\W)", "_").toUpperCase();

                n = n.replaceAll("1", "I");
                n = n.replaceAll("3", "III");

                icons.add(CardData.Icon.valueOf(n));
            }
        }
        return icons;
    }
    
    private String[] getNextParts(String s) {
        String[] v = new String[2];
        int i = s.indexOf("\\n");
        if(i == -1) {
            v[0] = s;
            v[1] = "";
        }
        else {
            v[0] = s.substring(0, i);
            v[1] = s.substring(i + 2, s.length());
        }
        
        if(v[0].equals("") && v[1].equals("")) {
            return v;
        }
        if(v[0].equals("")) {
            return getNextParts(v[1]);
        }
        return v;
    }
    
    /**
     * @param args the command line arguments
     */
    public static void main(String args[]) {
        /* Set the Nimbus look and feel */
        //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
        /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
         * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html 
         */
        try {
            for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
//                if ("Nimbus".equals(info.getName())) {
                if("Windows".equals(info.getName())) {
                    javax.swing.UIManager.setLookAndFeel(info.getClassName());
                    break;
                }
            }
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | javax.swing.UnsupportedLookAndFeelException ex) {
            java.util.logging.Logger.getLogger(DeckBuilderFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        }
        //</editor-fold>

        /* Create and display the form */
        java.awt.EventQueue.invokeLater(() -> {
            new DeckBuilderFrame().setVisible(true);
        });
    }
    
    /**
     * The LayerUI that handles the overlay for the JLayer containing the CardSheet.
     */
    class SheetLayerUI extends LayerUI<JComponent> {
        private final double DEGREES_90_IN_RAD = Math.PI / 2;
        private final SwingTimerTimingSource timeSource;
        private LayerTimingTarget painter;
        private Image icon;
        private boolean rotated;
        private CardPanel zoomedPanel;
        private CardData zoomedCard;
        
        public SheetLayerUI() {
            super();
            rotated = false;
            timeSource = new SwingTimerTimingSource();
            timeSource.init();
            Animator.setDefaultTimingSource(timeSource);
        }
        
        public CardData getZoomedCard() {
            return zoomedCard == null ? zoomedPanel.getCard() : zoomedCard;
        }
        
        public void zoomCard(CardData cd) throws IOException {
            boolean iconWasNull = (icon == null);

            icon = new CardPanel(cd.getImagePath()).getImage();
            painter = new ZoomInTimingTarget(cardSheetLayer, new Point(cardSheetLayer.getWidth() / 2, cardSheetLayer.getHeight() / 2), 0, 0);
            if(iconWasNull) {
                Animator a = new Animator.Builder().setDuration(250, TimeUnit.MILLISECONDS).addTarget((TimingTarget)painter).build();
                a.start();
            }
            else {
                painter.endNow();
                repaint();
            }
            zoomedCard = cd;
            if(zoomedPanel != null) {
                zoomedPanel.setMouseInside(false);
                zoomedPanel = null;
            }
// DEBUG
//            System.out.println(cd.getStringData());
        }

        /**
         * Paints the JLayer interface and will forward to the current TimingTarget animation if present.
         * @param g
         * @param c 
         */
        @Override
        public void paint(Graphics g, JComponent c) {
            super.paint(g, c);
            if(painter != null) {
                Graphics2D g2 = (Graphics2D)g.create();
                painter.paintToLayer(g2);
                g2.dispose();
            }
        }

        @Override
        public void installUI(JComponent c) {
            super.installUI(c);
            ((JLayer)c).setLayerEventMask(AWTEvent.MOUSE_EVENT_MASK + AWTEvent.MOUSE_WHEEL_EVENT_MASK);// + AWTEvent.MOUSE_MOTION_EVENT_MASK);
        }

        @Override
        public void uninstallUI(JComponent c) {
            super.uninstallUI(c);
            ((JLayer)c).setLayerEventMask(0);
        }

        @Override
        public void eventDispatched(AWTEvent e, JLayer<? extends JComponent> l) {
            MouseEvent me = (MouseEvent)e;
            if(icon == null) {
                // no icon so nothing is currently zoomed.
                if(me.getID() == MouseEvent.MOUSE_CLICKED && me.getSource() instanceof IconPanel) {
                    IconPanel ip = (IconPanel)me.getSource();
                    zoomedPanel = (CardPanel)ip.getParent();
                    if(zoomedPanel.getImage() != null) {
                        icon = zoomedPanel.getImage();

                        painter = new ZoomInTimingTarget(l, zoomedPanel.getImageLocation(), zoomedPanel.getWidthScale(), zoomedPanel.getHeightScale());
                        Animator a = new Animator.Builder().setDuration(250, TimeUnit.MILLISECONDS).addTarget((TimingTarget)painter).build();
                        a.start();
// DEBUG
//                        System.out.println(zoomedPanel.getCard().getStringData());
                    }
                }
                else if(me.getID() == MouseEvent.MOUSE_WHEEL) {
                    MouseWheelEvent mwe = (MouseWheelEvent)me;
                    cardSheet1.turnPage(mwe.getWheelRotation());
                }
            }
            else {
                // there is currently an icon/card zoomed in.
                if(me.getID() == MouseEvent.MOUSE_CLICKED) {
                    boolean clearLayer = true;

                    if(me.getButton() == MouseEvent.BUTTON3 && ((zoomedPanel != null && zoomedPanel.getCard().isLocation()) || (zoomedCard != null && zoomedCard.isLocation()))) {
                        // right-clicked a Location. rotate it to read the other side. do not clear the layer.
                        clearLayer = false;

                        painter = new RotateTimingTarget(l, rotated);
                        Animator a = new Animator.Builder().setDuration(100, TimeUnit.MILLISECONDS).addTarget((TimingTarget)painter).build();
                        a.start();
                        rotated = !rotated;
                    }

                    if(clearLayer) {
                        if(zoomedPanel != null) {
                            zoomedPanel.setMouseInside(false);
                        }
                        if(me.getSource() instanceof IconPanel) {
                            ((CardPanel)((IconPanel)me.getSource()).getParent()).setMouseInside(true);
                        }

                        painter = new ExitZoomTimingTarget(l, icon, rotated);
                        Animator a = new Animator.Builder().setDuration(150, TimeUnit.MILLISECONDS).addTarget((TimingTarget)painter).build();
                        a.start();
                        icon = null;
                        rotated = false;
                    }
                }
                else if(me.getID() == MouseEvent.MOUSE_WHEEL && zoomedPanel != null) {
                    try {
                        zoomedPanel.setMouseInside(false);
                        zoomedPanel = cardSheet1.getNextPanelInDirection(zoomedPanel, ((MouseWheelEvent)me).getWheelRotation());
                        icon = zoomedPanel.getImage();
                        zoomedPanel.setMouseInside(true);

                        rotated = false;
                        painter = new ZoomInTimingTarget(l, zoomedPanel.getImageLocation(), zoomedPanel.getWidthScale(), zoomedPanel.getHeightScale());
                        painter.endNow();
// DEBUG
//                        System.out.println(zoomedPanel.getCard().getStringData());
                        repaint();
                    } catch (IOException ex) {
                        Logger.getLogger(DeckBuilderFrame.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
                me.consume();
            }
        }
        
        abstract class LayerTimingTarget extends TimingTargetAdapter {
            protected double animationFraction;
            protected final JLayer<? extends JComponent> layer;

            public LayerTimingTarget(JLayer<? extends JComponent> layer) {
                this.layer = layer;
            }

            @Override
            public void timingEvent(Animator src, double fraction) {
                animationFraction = fraction;
                layer.repaint();
            }
            
            public void endNow() {
                animationFraction = 1.;
            }

            abstract public void paintToLayer(Graphics2D g);
        }
        
        class ZoomInTimingTarget extends LayerTimingTarget {
            private final Point startPoint;
            private final Point finishPoint;
            private final double widthScale;
            private final double heightScale;
            private final int iconWidth;
            private final int iconHeight;
            private final int layerWidth;
            private final int layerHeight;

            public ZoomInTimingTarget(JLayer<? extends JComponent> layer, Point startPoint, double scaleX, double scaleY) {
                super(layer);
                this.startPoint = startPoint;
                this.widthScale = scaleX;
                this.heightScale = scaleY;
                
                this.layerWidth = layer.getWidth();
                this.layerHeight = layer.getHeight();
                this.iconWidth = icon.getWidth(null);
                this.iconHeight = icon.getHeight(null);
                
                this.finishPoint = new Point(
                        layerWidth / 2 - iconWidth / 2,
                        layerHeight / 2 - iconHeight / 2
                );
            }

            @Override
            public void paintToLayer(Graphics2D g) {
                // Fade out
                int alpha = (int)(192. * animationFraction);
                Color dg = Color.darkGray.darker();
                g.setColor(new Color(dg.getRed(), dg.getGreen(), dg.getBlue(), alpha));
                g.fillRect(0, 0, layerWidth, layerHeight);
                //
                
                Point2D.Double drawPoint = new Point.Double(
                        (double)((finishPoint.x - startPoint.x) * animationFraction) + startPoint.x,
                        (double)((finishPoint.y - startPoint.y) * animationFraction) + startPoint.y);
                
                double drawScaleX = widthScale + ((1 - widthScale) * animationFraction);
                double drawScaleY = heightScale + ((1 - heightScale) * animationFraction);

                AffineTransform at = new AffineTransform();
                at.translate(drawPoint.x, drawPoint.y);
                at.scale(drawScaleX, drawScaleY);
                if(iconWidth > iconHeight) {
                    at.rotate(
                            DEGREES_90_IN_RAD - (DEGREES_90_IN_RAD * animationFraction),
                            iconHeight / 2,
                            iconHeight / 2
                    );
                }
                
                g.transform(at);
                g.setClip(new RoundRectangle2D.Double(0, 0, iconWidth, iconHeight, 14, 14));
                g.drawImage(icon, null, null);
            }
        }

        class RotateTimingTarget extends LayerTimingTarget {
            private final Point topLeftPoint;
            private final boolean isRotated;
            
            public RotateTimingTarget(JLayer<? extends JComponent> layer, boolean isRotated) {
                super(layer);
                this.isRotated = isRotated;
                
                topLeftPoint = new Point(
                        layer.getWidth() / 2 - icon.getWidth(null) / 2,
                        layer.getHeight() / 2 - icon.getHeight(null) / 2
                );
            }
            
            @Override
            public void paintToLayer(Graphics2D g) {
                // Fade out
                Color dg = Color.darkGray.darker();
                g.setColor(new Color(dg.getRed(), dg.getGreen(), dg.getBlue(), 192));
                g.fillRect(0, 0, layer.getWidth(), layer.getHeight());
                //
                
                AffineTransform at = new AffineTransform();
                at.translate(topLeftPoint.x, topLeftPoint.y);
                at.rotate((isRotated ? Math.PI : 0) + (Math.PI * animationFraction * (isRotated ? 1 : -1)), icon.getWidth(null) / 2, icon.getHeight(null) / 2);
                
                g.transform(at);
                g.setClip(new RoundRectangle2D.Double(0, 0, icon.getWidth(null), icon.getHeight(null), 14, 14));
                g.drawImage(icon, null, null);
            }
        }
        
        class ExitZoomTimingTarget extends LayerTimingTarget {
            private final Image img;
            private final boolean isRotated;
            private final Point imgPoint;
            
            public ExitZoomTimingTarget(JLayer<? extends JComponent> layer, Image img, boolean isRotated) {
                super(layer);
                this.img = img;
                this.isRotated = isRotated;
                
                imgPoint = new Point(
                        layer.getWidth() / 2 - img.getWidth(null) / 2,
                        layer.getHeight() / 2 - img.getHeight(null) / 2
                );
            }

            @Override
            public void paintToLayer(Graphics2D g) {
                // Fade in
                Color dg = Color.darkGray.darker();
                g.setColor(new Color(dg.getRed(), dg.getGreen(), dg.getBlue(), 192 - (int)(192. * animationFraction)));
                g.fillRect(0, 0, layer.getWidth(), layer.getHeight());
                //
                
                AffineTransform at = new AffineTransform();
                at.translate(imgPoint.x + (img.getWidth(null) / 2 * animationFraction), imgPoint.y + (img.getHeight(null) / 2 * animationFraction));
                at.scale(1. - animationFraction, 1. - animationFraction);
                if(isRotated) {
                    at.rotate(Math.PI, img.getWidth(null) / 2, img.getHeight(null) / 2);
                }
                g.drawImage(img, at, null);
            }
        }
    }

    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JDialog aboutDialog;
    private javax.swing.JMenuItem aboutMenuItem;
    private javax.swing.JLabel cardCountLabel;
    private javax.swing.JButton clearFilterButton;
    private javax.swing.JPopupMenu deckItemPopupMenu;
    private javax.swing.JList deckList;
    private javax.swing.JButton decrementCardButton;
    private javax.swing.JMenuItem exitMenuItem;
    private javax.swing.JMenu fileMenu;
    private javax.swing.JButton filterButton;
    private javax.swing.JTextField gameTextFilterField;
    private javax.swing.JMenu helpMenu;
    private javax.swing.JCheckBox includeLegacyCheckbox;
    private javax.swing.JButton incrementCardButton;
    private javax.swing.JLabel jLabel1;
    private javax.swing.JLabel jLabel2;
    private javax.swing.JLabel jLabel3;
    private javax.swing.JLabel jLabel4;
    private javax.swing.JLabel jLabel5;
    private javax.swing.JLabel jLabel6;
    private javax.swing.JLabel jLabel7;
    private javax.swing.JLabel jLabel8;
    private javax.swing.JMenuItem jMenuItem1;
    private javax.swing.JPanel jPanel1;
    private javax.swing.JScrollPane jScrollPane1;
    private javax.swing.JScrollPane jScrollPane2;
    private javax.swing.JScrollPane jScrollPane3;
    private javax.swing.JScrollPane jScrollPane4;
    private javax.swing.JTextArea jTextArea1;
    private javax.swing.JTextField loreFilterField;
    private javax.swing.JMenuBar menuBar;
    private javax.swing.JTextField nameFilterField;
    private javax.swing.JMenuItem openDeckMenuItem;
    private javax.swing.JButton removeCardButton;
    private javax.swing.JMenuItem saveDeckMenuItem;
    private javax.swing.JMenuItem setHolotablePathMenuItem;
    private javax.swing.JList setList;
    private com.goodjaerb.swccg.ui.SideFilterPanel sideFilterPanel1;
    private javax.swing.JList typeList;
    private com.goodjaerb.swccg.ui.ValidFieldFilterPanel validFieldCompareFilter1;
    private com.goodjaerb.swccg.ui.ValidFieldFilterPanel validFieldCompareFilter2;
    private com.goodjaerb.swccg.ui.ValidFieldFilterPanel validFieldCompareFilter3;
    private com.goodjaerb.swccg.ui.ValidFieldFilterPanel validFieldCompareFilter4;
    private javax.swing.JMenuItem viewCardMenuItem;
    // End of variables declaration//GEN-END:variables
}
